<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>ThreeLab · 网格光斑扩散 Demo</title>

<!-- 裸模块映射（本地 three 目录结构） -->
<script type="importmap">
{
  "imports": {
    "three": "./threejs/build/three.module.js",
    "three/addons/": "./threejs/examples/jsm/",
    "gsap": "./js/gsap/index.js"
  }
}
</script>

<style>
html,body{margin:0;padding:0;width:100%;height:100%;overflow:hidden;background:#000;}
#box{width:100%;height:100%;}
#watermark{position:fixed;right:10px;bottom:10px;color:#0ff;font-size:12px;pointer-events:none;opacity:.6;}
</style>
</head>
<body>
<div id="box"></div>
<div id="watermark">ThreeLab © 2024</div>

<script type="module">
/* ========================================================
   0. 模块导入
======================================================== */
import * as THREE        from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js'; 
import { gsap } from "gsap";
/* ========================================================
   1. 基础场景
======================================================== */
const box      = document.getElementById('box');
const scene    = new THREE.Scene();
scene.fog      = new THREE.Fog(0x102736, 1, 50);

const camera   = new THREE.PerspectiveCamera(45, box.clientWidth / box.clientHeight, 0.1, 100000);
camera.position.set(13, 12, 40);

const renderer = new THREE.WebGLRenderer({ antialias:true, alpha:true, logarithmicDepthBuffer:true });
renderer.setSize(box.clientWidth, box.clientHeight);
renderer.setClearColor(0x102736, 1);
box.appendChild(renderer.domElement);

const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;

/* ========================================================
   2. 配置参数
======================================================== */ 
const opt = {
  gridSize: 50,
  gridDivision: 20,
  gridColor: 0x1b4b70,
  shapeSize: .5,
  shapeColor: 0x2a5f8a,
  pointSize: .1,
  pointColor: 0x154d7d,
  diffuseSpeed: 10,
  diffuseWidth: 10,
  diffuseColor: 0x2e8bd9,
  diffuseDir: 0,            // 0 圆形，1 横向
  pointLayout: { row: 200, col: 200 }
};

/* ========================================================
   3. 地面
======================================================== */
const oceanTexture = new THREE.TextureLoader().load(
  `${FILE_HOST}images/bluebg.png`,
  tex => { tex.colorSpace = THREE.SRGBColorSpace; tex.wrapS = tex.wrapT = THREE.RepeatWrapping; },
  undefined,
  () => { tex.needsUpdate = true; }        // 失败时保持原色即可
);
const floor = new THREE.Mesh(
  new THREE.PlaneGeometry(20, 20),
  new THREE.MeshBasicMaterial({ map: oceanTexture, opacity: 1 })
); 
floor.rotateX(-Math.PI / 2)
floor.position.y = -0.7;
scene.add(floor);

/* ========================================================
   4. 网格 + 加号 + 点阵
======================================================== */
const gridGroup = new THREE.Group();
scene.add(gridGroup);

/* 4-1 网格线 */
const gridHelper = new THREE.GridHelper(opt.gridSize, opt.gridDivision, opt.gridColor, opt.gridColor);
 

/* 4-2 合并加号 */
const cellSize  = opt.gridSize / opt.gridDivision;
const half      = opt.gridSize / 2;
const lineW     = opt.shapeSize / 18;
const armLen    = opt.shapeSize / 3;
const plusGeoArr = [];
for (let r = 0; r <= opt.gridDivision; r++) {
  for (let c = 0; c <= opt.gridDivision; c++) {
    const shape = new THREE.Shape([
      new THREE.Vector2(-armLen, -lineW), new THREE.Vector2(-lineW, -lineW),
      new THREE.Vector2(-lineW, -armLen), new THREE.Vector2(lineW, -armLen),
      new THREE.Vector2(lineW, -lineW),   new THREE.Vector2(armLen, -lineW),
      new THREE.Vector2(armLen, lineW),   new THREE.Vector2(lineW, lineW),
      new THREE.Vector2(lineW, armLen),   new THREE.Vector2(-lineW, armLen),
      new THREE.Vector2(-lineW, lineW),   new THREE.Vector2(-armLen, lineW)
    ]);
    plusGeoArr.push(
      new THREE.ShapeGeometry(shape).translate(-half + r * cellSize, -half + c * cellSize, 0)
    );
  }
}
const plusMesh = new THREE.Mesh(
  mergeGeometries(plusGeoArr),
  new THREE.MeshBasicMaterial({ color: opt.shapeColor, side: THREE.DoubleSide })
);
plusMesh.rotateX(-Math.PI / 2)
plusMesh.position.y = 0.01;
gridGroup.add(plusMesh);

/* 4-3 扩散点阵 */
const rows = opt.pointLayout.row, cols = opt.pointLayout.col;
const posArr = new Float32Array(rows * cols * 3);
for (let i = 0; i < rows * cols; i++) {
  const r = i % rows, c = Math.floor(i / rows);
  posArr[i * 3]     = (r / (rows - 1)) * opt.gridSize - opt.gridSize / 2;
  posArr[i * 3 + 1] = 0;
  posArr[i * 3 + 2] = (c / (cols - 1)) * opt.gridSize - opt.gridSize / 2;
}
const pointGeo = new THREE.BufferGeometry();
pointGeo.setAttribute('position', new THREE.BufferAttribute(posArr, 3));

const pointMat = new THREE.PointsMaterial({
  color: opt.pointColor,
  size:  opt.pointSize,
  blending: THREE.NormalBlending
});

/* 着色器：扩散光环 */
let shaderRef;
pointMat.onBeforeCompile = shader => {
  shaderRef = shader;
  shader.uniforms = {
    ...shader.uniforms,
    uTime:  { value: 0 },
    uSpeed: { value: opt.diffuseSpeed },
    uWidth: { value: opt.diffuseWidth },
    uColor: { value: new THREE.Color(opt.diffuseColor) },
    uDir:   { value: opt.diffuseDir }
  };

  shader.vertexShader = shader.vertexShader.replace(
    'void main() {',
    'varying vec3 vPos; void main() { vPos = position;'
  );

  shader.fragmentShader = shader.fragmentShader.replace(
    'void main() {',
    `
    uniform float uTime, uSpeed, uWidth, uDir;
    uniform vec3 uColor;
    varying vec3 vPos;
    void main() {
    `
  ).replace(
    '#include <opaque_fragment>',
    `
    float radius = uTime * uSpeed;
    float width  = min(uWidth, uTime * 5.0);
    vec2  center = vec2(0.0);
    float dist   = uDir == 1.0 ? abs(vPos.x) : distance(vPos.xz, center);

    if (dist > radius && dist < radius + 2.0 * width) {
      float pct = dist < radius + width
        ? (dist - radius) / width
        : (dist - radius - width) / width;
      vec3 outCol = dist < radius + width
        ? mix(outgoingLight, uColor, pct)
        : mix(uColor, outgoingLight, pct);
      gl_FragColor = vec4(outCol, diffuseColor.a);
    } else {
      gl_FragColor = vec4(outgoingLight, diffuseColor.a);
    }
    `
  );
};

const points = new THREE.Points(pointGeo, pointMat);

gridGroup.add(points);

/* ========================================================
   5. 相机动画
======================================================== */
gsap.timeline()
  .to(camera.position, { duration: 2.5, x: -20.46, y: 19.30, z: 58.38, ease: 'circ.out' })
  .to(camera.position, { duration: 2.5, x: -0.25,  y: 12.40, z: 14.65, ease: 'circ.out' });

/* ========================================================
   6. 渲染循环
======================================================== */
const clock = new THREE.Clock();
const resetTime = opt.gridSize / opt.diffuseSpeed;

function animate() {
  requestAnimationFrame(animate);
  if (shaderRef) {
    shaderRef.uniforms.uTime.value += clock.getDelta();
    if (shaderRef.uniforms.uTime.value > resetTime) shaderRef.uniforms.uTime.value = 0;
  }
  controls.update();
  renderer.render(scene, camera);
}
animate();

/* ========================================================
   7. 自适应
======================================================== */
window.addEventListener('resize', () => {
  camera.aspect = box.clientWidth / box.clientHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(box.clientWidth, box.clientHeight);
});

/* ========================================================
   8. 控制台水印
======================================================== */
console.log(
  '%cThreeLab © 2024 - Grid Diffusion Demo',
  'color:#0ff;font-size:14px;background:#000;padding:4px 8px;border-radius:4px;'
);
</script>
</body>
</html>